 /*******************************************************************************
  * Copyright (c) 2005, 2006 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.ui.internal.texteditor;


 import org.eclipse.swt.SWT;
 import org.eclipse.swt.custom.StyledText;
 import org.eclipse.swt.custom.VerifyKeyListener;
 import org.eclipse.swt.events.FocusEvent;
 import org.eclipse.swt.events.FocusListener;
 import org.eclipse.swt.events.MouseEvent;
 import org.eclipse.swt.events.MouseListener;
 import org.eclipse.swt.events.VerifyEvent;

 import org.eclipse.core.commands.ExecutionEvent;
 import org.eclipse.core.commands.ExecutionException;
 import org.eclipse.core.commands.IExecutionListener;
 import org.eclipse.core.commands.NotHandledException;

 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.ListenerList;
 import org.eclipse.core.runtime.Status;

 import org.eclipse.jface.text.ITextViewer;

 import org.eclipse.ui.PlatformUI;
 import org.eclipse.ui.commands.ICommandService;

 /**
  * Exit strategy for commands that want to fold repeated execution into one compound edit. See
  * {@link org.eclipse.jface.text.IRewriteTarget#endCompoundChange() IRewriteTarget.endCompoundChange}.
  * As long as a strategy is installed on an {@link ITextViewer}, it will detect the end of a
  * compound operation when any of the following conditions becomes true:
  * <ul>
  * <li>the viewer's text widget loses the keyboard focus</li>
  * <li>the mouse is clicked or double clicked inside the viewer's widget</li>
  * <li>a command other than the ones specified is executed</li>
  * <li>the viewer receives any key events that are not modifier combinations</li>
  * </ul>
  * <p>
  * If the end of a compound edit is detected, any registered {@link ICompoundEditListener}s are
  * notified and the strategy is disarmed (spring-loaded).
  * </p>
  *
  * @since 3.1
  */
 public final class CompoundEditExitStrategy {
     /**
      * Listens for events that may trigger the end of a compound edit.
      */
     private final class EventListener implements MouseListener, FocusListener, VerifyKeyListener, IExecutionListener {

         /*
          * @see org.eclipse.swt.events.MouseListener#mouseDoubleClick(org.eclipse.swt.events.MouseEvent)
          */
         public void mouseDoubleClick(MouseEvent e) {
             // mouse actions end the compound change
 fireEndCompoundEdit();
         }

         /*
          * @see org.eclipse.swt.events.MouseListener#mouseDown(org.eclipse.swt.events.MouseEvent)
          */
         public void mouseDown(MouseEvent e) {
             // mouse actions end the compound change
 fireEndCompoundEdit();
         }

         public void mouseUp(MouseEvent e) {}

         public void focusGained(FocusEvent e) {}

         /*
          * @see org.eclipse.swt.events.FocusListener#focusLost(org.eclipse.swt.events.FocusEvent)
          */
         public void focusLost(FocusEvent e) {
             // losing focus ends the change
 fireEndCompoundEdit();
         }

         public void notHandled(String commandId, NotHandledException exception) {}

         public void postExecuteFailure(String commandId, ExecutionException exception) {}

         public void postExecuteSuccess(String commandId, Object returnValue) {}

         /*
          * @see org.eclipse.core.commands.IExecutionListener#preExecute(java.lang.String, org.eclipse.core.commands.ExecutionEvent)
          */
         public void preExecute(String commandId, ExecutionEvent event) {
             // any command other than the known ones end the compound change
 for (int i= 0; i < fCommandIds.length; i++) {
                 if (commandId.equals(fCommandIds[i]))
                     return;
             }
             fireEndCompoundEdit();
         }

         /*
          * @see org.eclipse.swt.custom.VerifyKeyListener#verifyKey(org.eclipse.swt.events.VerifyEvent)
          */
         public void verifyKey(VerifyEvent event) {
             // any key press that is not a modifier combo ends the compound change
 final int maskWithoutShift= SWT.MODIFIER_MASK & ~SWT.SHIFT;
             if ((event.keyCode & SWT.MODIFIER_MASK) == 0 && (event.stateMask & maskWithoutShift) == 0)
                 fireEndCompoundEdit();
         }
         
     }

     private final String [] fCommandIds;
     private final EventListener fEventListener= new EventListener();
     private final ListenerList fListenerList= new ListenerList(ListenerList.IDENTITY);

     private ITextViewer fViewer;
     private StyledText fWidgetEventSource;

     /**
      * Creates a new strategy, equivalent to calling
      * {@linkplain #CompoundEditExitStrategy(String[]) CompoundEditExitStrategy(new String[] &#x7b; commandId &#x7d;)}.
      *
      * @param commandId the command id of the repeatable command
      */
     public CompoundEditExitStrategy(String commandId) {
         if (commandId == null)
             throw new NullPointerException ("commandId"); //$NON-NLS-1$
 fCommandIds= new String [] {commandId};
     }
     
     /**
      * Creates a new strategy, ending upon execution of any command other than the ones
      * specified.
      *
      * @param commandIds the ids of the repeatable commands
      */
     public CompoundEditExitStrategy(String [] commandIds) {
         for (int i= 0; i < commandIds.length; i++) {
             if (commandIds[i] == null)
                 throw new NullPointerException ("commandIds[" + i + "]"); //$NON-NLS-1$ //$NON-NLS-2$
 }
         fCommandIds= new String [commandIds.length];
         System.arraycopy(commandIds, 0, fCommandIds, 0, commandIds.length);
     }
     
     /**
      * Installs the receiver on <code>viewer</code> and arms it. After this call returns, any
      * registered listeners will be notified if a compound edit ends.
      *
      * @param viewer the viewer to install on
      */
     public void arm(ITextViewer viewer) {
         disarm();
         if (viewer == null)
             throw new NullPointerException ("editor"); //$NON-NLS-1$
 fViewer= viewer;
         addListeners(fViewer);
     }
     
     /**
      * Disarms the receiver. After this call returns, any registered listeners will be not be
      * notified any more until <code>install</code> is called again. Note that the listeners are
      * not removed.
      * <p>
      * Note that the receiver is automatically disarmed when the end of a compound edit has
      * been detected and before the listeners are notified.
      * </p>
      */
     public void disarm() {
         if (isInstalled()) {
             removeListeners(fViewer);
             fViewer= null;
         }
     }

     private void addListeners(ITextViewer viewer) {
         fWidgetEventSource= viewer.getTextWidget();
         if (fWidgetEventSource != null) {
             fWidgetEventSource.addVerifyKeyListener(fEventListener);
             fWidgetEventSource.addMouseListener(fEventListener);
             fWidgetEventSource.addFocusListener(fEventListener);
         }

         ICommandService commandService= (ICommandService)PlatformUI.getWorkbench().getAdapter(ICommandService.class);
         if (commandService != null)
             commandService.addExecutionListener(fEventListener);
     }
     
     private void removeListeners(ITextViewer editor) {
         ICommandService commandService= (ICommandService)PlatformUI.getWorkbench().getAdapter(ICommandService.class);
         if (commandService != null)
             commandService.removeExecutionListener(fEventListener);
         
         if (fWidgetEventSource != null) {
             fWidgetEventSource.removeFocusListener(fEventListener);
             fWidgetEventSource.removeMouseListener(fEventListener);
             fWidgetEventSource.removeVerifyKeyListener(fEventListener);
             fWidgetEventSource= null;
         }
     }

     private boolean isInstalled() {
         return fViewer != null;
     }
     
     private void fireEndCompoundEdit() {
         disarm();
         Object [] listeners= fListenerList.getListeners();
         for (int i= 0; i < listeners.length; i++) {
             ICompoundEditListener listener= (ICompoundEditListener) listeners[i];
             try {
                 listener.endCompoundEdit();
             } catch (Exception e) {
                 IStatus status= new Status(IStatus.ERROR, TextEditorPlugin.PLUGIN_ID, IStatus.OK, "listener notification failed", e); //$NON-NLS-1$
 TextEditorPlugin.getDefault().getLog().log(status);
             }
         }
     }
     
     /**
      * Adds a compound edit listener. Multiple registration is possible. Note that the receiver is
      * automatically disarmed before the listeners are notified.
      *
      * @param listener the new listener
      */
     public void addCompoundListener(ICompoundEditListener listener) {
         fListenerList.add(listener);
     }
     
     /**
      * Removes a compound edit listener. If <code>listener</code> is registered multiple times, an
      * arbitrary instance is removed. If <code>listener</code> is not currently registered,
      * nothing happens.
      *
      * @param listener the listener to be removed.
      */
     public void removeCompoundListener(ICompoundEditListener listener) {
         fListenerList.remove(listener);
     }
     
 }

